import java.awt.Component;
import java.io.*;

/**
 * The main class of the P3 exercise. This class is only partially complete.
 */
public class Simulator implements Constants
{
	/** The queue of events to come */
	private EventQueue eventQueue;
	/** Reference to the memory unit */
	private Memory memory;
	/** Reference to CPU unit */
	private Cpu cpu;
	/** Reference to the GUI interface */
	private Gui gui;
	/** Reference to the statistics collector */
	private Statistics statistics;
	/** The global clock */
	private long clock;
	/** The maximum CPU time allowed per process */
	private long maxCpuTime;
	/** The length of the simulation */
	private long simulationLength;
	/** The average length between process arrivals */
	private long avgArrivalInterval;
	/** The current active process running in CPU*/
	private Process activeProcess;
	
	private Process ioProcess;
	/** Reference to the io unit */
	private IO io;
	/** Field used to determine next in queue, when queue is empty*/
	private long nextArrivalTime;
	/** Average input/output delay, set by user on run*/
	private long avgIoTime;
	
	private Process ioActive;

	/**
	 * Constructs a scheduling simulator with the given parameters.
	 * @param memoryQueue			The memory queue to be used.
	 * @param cpuQueue				The CPU queue to be used.
	 * @param ioQueue				The I/O queue to be used.
	 * @param memorySize			The size of the memory.
	 * @param maxCpuTime			The maximum time quant used by the RR algorithm.
	 * @param avgIoTime				The average length of an I/O operation.
	 * @param simulationLength		The length of the simulation.
	 * @param avgArrivalInterval	The average time between process arrivals.
	 * @param gui					Reference to the GUI interface.
	 */
	public Simulator(Queue memoryQueue, Queue cpuQueue, Queue ioQueue, long memorySize,
			long mCpuTime, long avgIoTime, long simulationLength, long avgArrivalInterval, Gui gui) {
		this.simulationLength = simulationLength;
		this.avgIoTime = avgIoTime;
		this.avgArrivalInterval = avgArrivalInterval;
		this.gui = gui;
		maxCpuTime = mCpuTime;
		statistics = new Statistics();
		eventQueue = new EventQueue();
		memory = new Memory(memoryQueue, memorySize, statistics);
		cpu = new Cpu(cpuQueue, maxCpuTime, statistics, gui);
		io = new IO(ioQueue, avgIoTime, statistics, gui);
		clock = 0;
		activeProcess = null;
		ioActive = null;
	}

	/**
	 * Starts the simulation. Contains the main loop, processing events.
	 * This method is called when the "Start simulation" button in the
	 * GUI is clicked.
	 */
	public void simulate() {
		System.out.print("Simulating...");
		// Genererate the first process arrival event
		eventQueue.insertEvent(new Event(NEW_PROCESS, 0));
		eventQueue.insertEvent(new Event(SWITCH_PROCESS, 0)); //Generate first switch event
		eventQueue.insertEvent(new Event(IO_REQUEST, 0));
		// Process events until the simulation length is exceeded:
		while (clock < simulationLength && !eventQueue.isEmpty()) {
			// Find the next event
			Event event = eventQueue.getNextEvent();
			// Find out how much time that passed...
			long timeDifference = event.getTime()-clock;
			// ...and update the clock.
			clock = event.getTime();
			// Let the memory unit and the GUI know that time has passed
			memory.timePassed(timeDifference);
			gui.timePassed(timeDifference);
			cpu.timePassed(timeDifference);
			io.timePassed(timeDifference);
			// Deal with the event
			if (clock < simulationLength) {
				//System.out.println("Event Time - " + event.getTime());
				processEvent(event);
			}
		}
		System.out.println("..done.");
		// End the simulation by printing out the required statistics
		statistics.printReport(simulationLength);
	}

	/**
	 * Processes an event by inspecting its type and delegating
	 * the work to the appropriate method.
	 * @param event	The event to be processed.
	 */
	private void processEvent(Event event) {
		switch (event.getType()) {
		case NEW_PROCESS:
			createProcess();
			break;
		case SWITCH_PROCESS:
			switchProcess();
			break;
		case END_PROCESS:
			endProcess();
			break;
		case IO_REQUEST:
			processIoRequest();
			break;
		case END_IO:
			endIoOperation();
			break;
		case STOP_SIM:
			stopSimulation();
			break;
		}

	}

	/**
	 * Simulates a process arrival/creation.
	 */
	private void createProcess() {
		// Create a new process
		Process newProcess = new Process(memory.getMemorySize(), clock);
		memory.insertProcess(newProcess, clock);
		flushMemoryQueue();
		// Add an event for the next process arrival
		nextArrivalTime = clock + 1 + (long)(2*Math.random()*avgArrivalInterval);
		eventQueue.insertEvent(new Event(NEW_PROCESS, nextArrivalTime));
		// Update statistics
		statistics.nofCreatedProcesses++;
	}

	/**
	 * Transfers processes from the memory queue to the ready queue as long as there is enough
	 * memory for the processes.
	 */
	private void flushMemoryQueue() {
		Process p = memory.checkMemory(clock);
		// As long as there is enough memory, processes are moved from the memory queue to the cpu queue
		while(p != null) {
			cpu.insertProcess(p);
			// Try to use the freed memory:
			flushMemoryQueue();
			// Update statistics
			p.updateStatistics(statistics);

			// Check for more free memory
			p = memory.checkMemory(clock);
		}
	}

	/**
	 * Simulates a process switch.
	 */
	private void switchProcess() {
		if (activeProcess != null){ //Used to show correct GUI of active process
			cpu.insertProcess(activeProcess); //insert active Process back in queue
			activeProcess.setTimeSpentInReadyQueue(activeProcess.getTimeSpentInReadyQueue() + //Used in statistics
					(activeProcess.getTimeExitedCpuQueue() - activeProcess.getTimeEnteredCpuQueue()));
			activeProcess.setTimeEnteredCpuQueue(clock);
			activeProcess = null;
		}
		activeProcess = cpu.getNextProcess();
		gui.setCpuActive(activeProcess); //Updates GUI to current active Process
		statistics.nofForcedProcessSwitches++; //Updates statistics
		switch(cpu.doProcessing(clock)) {
		case CPUFINISHED:
			if (!isEventTooLong((clock + cpu.calcRemCpuNeeded()))){
				eventQueue.insertEvent(new Event(END_PROCESS, clock)); //Create event to end process in this clock instance
				eventQueue.insertEvent(new Event(SWITCH_PROCESS, (clock + cpu.calcRemCpuNeeded()))); //Create a switch event
			}
			else { // If last event surpasses allowed simulation length, set last event to that time
				eventQueue.insertEvent(new Event(END_PROCESS, clock)); //Create event to end process in this clock instance
				eventQueue.insertEvent(new Event(STOP_SIM, (simulationLength))); 
			}
			break;
		case CPUTIMEOUT:
			if (!isEventTooLong((clock+maxCpuTime))){
				eventQueue.insertEvent(new Event(SWITCH_PROCESS, clock+maxCpuTime));//switch process after maxCpuTime
				statistics.totalCpuTimeSpentProcessing += (maxCpuTime); //Update statistics
			}
			else {// If last event surpasses allowed simulation length, set last event to that time
				eventQueue.insertEvent(new Event(STOP_SIM, (simulationLength))); 
			}
			
			break;
		case -1: // If no processes in queue
			eventQueue.insertEvent(new Event(SWITCH_PROCESS, (clock+1)));//switch process after maxCpuTime
			flushMemoryQueue();
			//System.out.println("No processes in CPU queue!");
			break;
			}
	}

	/**
	 * Ends the active process, and deallocates any resources allocated to it.
	 */
	private void endProcess() {
		if (activeProcess != null){
			cpu.setNofTimesInCpuQueue(); //Increment statistic counter
			activeProcess.setStopTime(clock); //Set the process' end time
			statistics.averageTimeInSystem += ((activeProcess.getStopTime()) - (activeProcess.getStartTime())); //Update statistics
			//System.out.println("Time spent waiting: " + activeProcess.getTimeSpentInReadyQueue());
			statistics.totalCpuTimeSpentProcessingFinished += activeProcess.getTimeSpentInCpu(); //Update statistics
			statistics.averageTimeWaitingInCpuQueue += activeProcess.getTimeSpentInReadyQueue(); //Update statistics
			statistics.nofCompletedProcesses++; //Update statistics
			statistics.averageTimeWaitingInIOQueue += (activeProcess.getTimeExitedIoQueue() - activeProcess.getTimeEnteredIoQueue());
			memory.checkMemory(clock);
			memory.processCompleted(activeProcess); //Removes used memory
			flushMemoryQueue();
			activeProcess = null;
			cpu.endActiveProcess();
			//System.out.println("A completed process... Free memory: " + (memory.getFreeMemory()));
		}

	}

	/**
	 * Processes an event signifying that the active process needs to
	 * perform an I/O operation.
	 */
	private void processIoRequest() {
		
		ioProcess = cpu.getActiveProcess(clock);
		
		
		if(ioActive == null){
			if(ioProcess != null){
				memory.processCompleted(ioProcess);
				ioActive = ioProcess;
				io.insertProcess(ioProcess); // puts the current process in CPU into IO queue
				activeProcess = null;
				cpu.endActiveProcess();
				gui.setCpuActive(null);
				io.getNextProcess();
				
			}	
			
			switch(io.doProcessing()){
			case END_IO:
				ioProcess.setTimeExitedIoQueue(clock);
				
				gui.setIoActive(ioProcess);
				eventQueue.insertEvent(new Event(END_IO, clock+ioProcess.getAvgIoInterval()));
				eventQueue.insertEvent(new Event(IO_REQUEST, clock+io.getIoDelay()));
				break;
				
			case -1:
				eventQueue.insertEvent(new Event(IO_REQUEST, clock+io.getIoDelay()));
				break;

			}
		}
		
		else if (ioActive != null) {
			if(ioProcess != null){
				memory.processCompleted(ioProcess);
				io.insertProcess(ioProcess); // puts the current process in CPU into IO queue
				activeProcess = null; 
				cpu.endActiveProcess();
				gui.setCpuActive(null);
				ioProcess.setTimeEnteredIoQueue(clock);
				}
			eventQueue.insertEvent(new Event(IO_REQUEST, clock+io.getIoDelay()));
		}
		
	}


	/**
	 * Processes an event signifying that the process currently doing I/O
	 * is done with its I/O operation.
	 */
	private void endIoOperation() {
		if(memory.getFreeMemory() >= ioActive.getMemoryNeeded()){
			gui.setIoActive(null);
			cpu.insertProcess(ioActive);
			memory.freeMemory -= ioActive.getMemoryNeeded();
			statistics.totalIOTimeSpentProcessingFinished += ioActive.getTimeSpentInIo();//Update statistics
			statistics.averageTimeWaitingInIOQueue += ioActive.getTimeSpentWaitingForIo(); //Update statistics
			statistics.nofCompletedIOProcesses++; //Update statistics
			ioActive = null;
			io.endActiveProcess();
			io.setNofTimesInIOQueue(); //Increment statistic counter
			flushMemoryQueue();
		}else{
			eventQueue.insertEvent(new Event(END_IO, clock+1));
		}
		
	}

	/**
	 * Reads a number from the an input reader.
	 * @param reader	The input reader from which to read a number.
	 * @return			The number that was inputted.
	 */
	public static long readLong(BufferedReader reader) {
		try {
			return Long.parseLong(reader.readLine());
		} catch (IOException ioe) {
			return 100;
		} catch (NumberFormatException nfe) {
			return 0;
		}
	}
	
	/**
	 * Checks if added event surpasses the allowed simulation length
	 * @param time
	 * @return
	 */
	public boolean isEventTooLong(long time){
		if (time < simulationLength){
			return false;
		}
		return true;
	}
	
	private void stopSimulation(){
		System.out.println();
		System.out.println("Simulation time reached...");
	}
	
	/**
	 * The startup method. Reads relevant parameters from the standard input,
	 * and starts up the GUI. The GUI will then start the simulation when
	 * the user clicks the "Start simulation" button.
	 * @param args	Parameters from the command line, they are ignored.
	 */
	public static void main(String args[]) {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		System.out.println("Please input system parameters: ");

		System.out.print("Memory size (KB): ");
		long memorySize = readLong(reader);
		while(memorySize < 400) {
			System.out.println("Memory size must be at least 400 KB. Specify memory size (KB): ");
			memorySize = readLong(reader);
		}

		System.out.print("Maximum uninterrupted cpu time for a process (ms): ");
		long maxCpuTime = readLong(reader);

		System.out.print("Average I/O operation time (ms): ");
		long avgIoTime = readLong(reader);

		System.out.print("Simulation length (ms): ");
		long simulationLength = readLong(reader);
		while(simulationLength < 1) {
			System.out.println("Simulation length must be at least 1 ms. Specify simulation length (ms): ");
			simulationLength = readLong(reader);
		}

		System.out.print("Average time between process arrivals (ms): ");
		long avgArrivalInterval = readLong(reader);

		SimulationGui gui = new SimulationGui(memorySize, maxCpuTime, avgIoTime, simulationLength, avgArrivalInterval);
	}
}
